/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.ops4j.pax.web.service.undertow.internal;

import java.security.GeneralSecurityException;
import java.security.InvalidParameterException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.Security;
import java.security.cert.CRL;
import java.security.cert.CertPathBuilder;
import java.security.cert.CertPathBuilderResult;
import java.security.cert.CertPathValidator;
import java.security.cert.CertStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.X509CertSelector;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.concurrent.atomic.AtomicLong;

import org.ops4j.pax.web.annotations.Review;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Convenience class to handle validation of certificates, aliases and keystores
 *
 * Allows specifying Certificate Revocation List (CRL), as well as enabling
 * CRL Distribution Points Protocol (CRLDP) certificate extension support,
 * and also enabling On-Line Certificate Status Protocol (OCSP) support.
 *
 * IMPORTANT: at least one of the above mechanisms *MUST* be configured and
 * operational, otherwise certificate validation *WILL FAIL* unconditionally.
 */
@Review("Is it needed? It's a copy from Jetty...")
public class CertificateValidator {

	private static final Logger LOG = LoggerFactory.getLogger(CertificateValidator.class);
	private static AtomicLong __aliasCount = new AtomicLong();

	private KeyStore _trustStore;
	private Collection<? extends CRL> _crls;

	/** Maximum certification path length (n - number of intermediate certs, -1 for unlimited) */
	private int _maxCertPathLength = -1;
	/** CRL Distribution Points (CRLDP) support */
	private boolean _enableCRLDP = false;
	/** On-Line Certificate Status Protocol (OCSP) support */
	private boolean _enableOCSP = false;
	/** Location of OCSP Responder */
	private String _ocspResponderURL;

	/**
	 * creates an instance of the certificate validator
	 *
	 * @param trustStore the truststore to use
	 * @param crls the Certificate Revocation List to use
	 */
	public CertificateValidator(KeyStore trustStore, Collection<? extends CRL> crls) {
		if (trustStore == null) {
			throw new InvalidParameterException("TrustStore must be specified for CertificateValidator.");
		}

		_trustStore = trustStore;
		_crls = crls;
	}

	/**
	 * validates all aliases inside of a given keystore
	 *
	 * @param keyStore the keystore to validate
	 * @throws CertificateException if keystore error and unable to validate
	 */
	public void validate(KeyStore keyStore) throws CertificateException {
		try {
			Enumeration<String> aliases = keyStore.aliases();

			for (; aliases.hasMoreElements(); ) {
				String alias = aliases.nextElement();

				validate(keyStore, alias);
			}

		} catch (KeyStoreException kse) {
			throw new CertificateException("Unable to retrieve aliases from keystore", kse);
		}
	}

	/**
	 * validates a specific alias inside of the keystore being passed in
	 *
	 * @param keyStore the keystore to validate
	 * @param keyAlias the keyalias in the keystore to valid with
	 * @return the keyAlias if valid
	 * @throws CertificateException if keystore error and unable to validate
	 */
	public String validate(KeyStore keyStore, String keyAlias) throws CertificateException {
		String result = null;

		if (keyAlias != null) {
			try {
				validate(keyStore, keyStore.getCertificate(keyAlias));
			} catch (KeyStoreException kse) {
				LOG.error("Error while valid keystore", kse);
				throw new CertificateException("Unable to validate certificate" +
						" for alias [" + keyAlias + "]: " + kse.getMessage(), kse);
			}
			result = keyAlias;
		}

		return result;
	}

	/**
	 * validates a specific certificate inside of the keystore being passed in
	 *
	 * @param keyStore the keystore to validate against
	 * @param cert the certificate to validate
	 * @throws CertificateException if keystore error and unable to validate
	 */
	public void validate(KeyStore keyStore, Certificate cert) throws CertificateException {
		Certificate[] certChain = null;

		if (cert != null && cert instanceof X509Certificate) {
			((X509Certificate) cert).checkValidity();

			String certAlias = null;
			try {
				if (keyStore == null) {
					throw new InvalidParameterException("Keystore cannot be null");
				}

				certAlias = keyStore.getCertificateAlias((X509Certificate) cert);
				if (certAlias == null) {
					certAlias = "JETTY" + String.format("%016X", __aliasCount.incrementAndGet());
					keyStore.setCertificateEntry(certAlias, cert);
				}

				certChain = keyStore.getCertificateChain(certAlias);
				if (certChain == null || certChain.length == 0) {
					throw new IllegalStateException("Unable to retrieve certificate chain");
				}
			} catch (KeyStoreException kse) {
				LOG.error("Error while valid keystore", kse);
				throw new CertificateException("Unable to validate certificate" +
						(certAlias == null ? "" : " for alias [" + certAlias + "]") + ": " + kse.getMessage(), kse);
			}

			validate(certChain);
		}
	}

	public void validate(Certificate[] certChain) throws CertificateException {
		try {
			ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>();
			for (Certificate item : certChain) {
				if (item == null) {
					continue;
				}

				if (!(item instanceof X509Certificate)) {
					throw new IllegalStateException("Invalid certificate type in chain");
				}

				certList.add((X509Certificate) item);
			}

			if (certList.isEmpty()) {
				throw new IllegalStateException("Invalid certificate chain");

			}

			X509CertSelector certSelect = new X509CertSelector();
			certSelect.setCertificate(certList.get(0));

			// Configure certification path builder parameters
			PKIXBuilderParameters pbParams = new PKIXBuilderParameters(_trustStore, certSelect);
			pbParams.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(certList)));

			// Set maximum certification path length
			pbParams.setMaxPathLength(_maxCertPathLength);

			// Enable revocation checking
			pbParams.setRevocationEnabled(true);

			// Set static Certificate Revocation List
			if (_crls != null && !_crls.isEmpty()) {
				pbParams.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(_crls)));
			}

			// Enable On-Line Certificate Status Protocol (OCSP) support
			if (_enableOCSP) {
				Security.setProperty("ocsp.enable", "true");
			}
			// Enable Certificate Revocation List Distribution Points (CRLDP) support
			if (_enableCRLDP) {
				System.setProperty("com.sun.security.enableCRLDP", "true");
			}

			// Build certification path
			CertPathBuilderResult buildResult = CertPathBuilder.getInstance("PKIX").build(pbParams);

			// Validate certification path
			CertPathValidator.getInstance("PKIX").validate(buildResult.getCertPath(), pbParams);
		} catch (GeneralSecurityException gse) {
			LOG.error("Error while valid keystore", gse);
			throw new CertificateException("Unable to validate certificate: " + gse.getMessage(), gse);
		}
	}

	public KeyStore getTrustStore() {
		return _trustStore;
	}

	public Collection<? extends CRL> getCrls() {
		return _crls;
	}

	/**
	 * @return Maximum number of intermediate certificates in
	 * the certification path (-1 for unlimited)
	 */
	public int getMaxCertPathLength() {
		return _maxCertPathLength;
	}

	/* ------------------------------------------------------------ */

	/**
	 * @param maxCertPathLength
	 *            maximum number of intermediate certificates in
	 *            the certification path (-1 for unlimited)
	 */
	public void setMaxCertPathLength(int maxCertPathLength) {
		_maxCertPathLength = maxCertPathLength;
	}

	/* ------------------------------------------------------------ */

	/**
	 * @return true if CRL Distribution Points support is enabled
	 */
	public boolean isEnableCRLDP() {
		return _enableCRLDP;
	}

	/* ------------------------------------------------------------ */

	/** Enables CRL Distribution Points Support
	 * @param enableCRLDP true - turn on, false - turns off
	 */
	public void setEnableCRLDP(boolean enableCRLDP) {
		_enableCRLDP = enableCRLDP;
	}

	/* ------------------------------------------------------------ */

	/**
	 * @return true if On-Line Certificate Status Protocol support is enabled
	 */
	public boolean isEnableOCSP() {
		return _enableOCSP;
	}

	/* ------------------------------------------------------------ */

	/** Enables On-Line Certificate Status Protocol support
	 * @param enableOCSP true - turn on, false - turn off
	 */
	public void setEnableOCSP(boolean enableOCSP) {
		_enableOCSP = enableOCSP;
	}

	/* ------------------------------------------------------------ */

	/**
	 * @return Location of the OCSP Responder
	 */
	public String getOcspResponderURL() {
		return _ocspResponderURL;
	}

	/* ------------------------------------------------------------ */

	/** Set the location of the OCSP Responder.
	 * @param ocspResponderURL location of the OCSP Responder
	 */
	public void setOcspResponderURL(String ocspResponderURL) {
		_ocspResponderURL = ocspResponderURL;
	}
}

